<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Api 文档</title>
    <!-- <link href="/static/ui365-pro/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> -->
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css" rel="stylesheet"
        title="断网的情况下，下载一个 bootstrap 4.x 的样式文件到本地并引入就可以正常使用了，无需任何 bootstrap 的 js 文件">
    <script src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js"
        title="参数签名需要使用 md5 方法，因此需要引入 md5 插件"></script>
    <!-- 引入项目的外部配置文件 -->
    <script src="apidata/project_example.js" title="把接口数据剥离到外部配置文件中，便于维护和管理"></script>
    <script>
        // 版本号（最后更新：适配移动端）
        const VERSION = '1.1.5';
        // 浏览器传参：?proj=demo
        let projId = new URLSearchParams(document.location.search).get('proj');
        // 业务项目配置（在浏览器加入参数：?proj=example）
        function getProjectConfig(projId) {
            if (!projId) {
                // 设置默认项目
                projId = 'demo';
            }
            // 所有项目
            let projects = [
                {
                    name: 'demo', text: '演示项目', get_config: function () {
                        // getDemoProject() 在页面底部定义。
                        return getDemoProject()
                    }
                },
                {
                    name: 'example', text: '示例项目', get_config: function () {
                        // project_example 在 apidata/project_example.js 中定义了 const project_example={...}。
                        return typeof (project_example) == 'undefined' ? null : project_example;
                    }
                },
                {
                    name: 'main', text: '主要项目', get_config: function () {
                        return typeof (project_main) == 'undefined' ? null : project_main;
                    }
                },
            ]
            if (projId == 'ALL') {
                return projects;
            }
            let data = null;
            projects.forEach(item => {
                if (item.name == projId) {
                    data = item.get_config();
                    return;
                }
            })
            return data;
        }

    </script>
    <style>
        body {
            margin: 10px;
            font-size: 14px;
        }

        aside li {
            list-style: none;
            line-height: 24px;
        }

        aside li ul {
            margin-left: -22px
        }

        aside .aside-folder-switcher {
            margin-right: 5px;
        }

        aside a.link {
            line-height: 24px;
            color: var(--dark);
            display: block;
            max-width: 240px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            flex-grow: 1;
        }

        aside .icon {
            color: dodgerblue;
            font-size: 12px;
        }

        th {
            white-space: nowrap !important;
        }

        #jsonCode {
            height: calc(100% - 20px)
        }
    </style>
</head>

<body>
    <div class="container-fluid">
        <header>
            <div class="row">
                <h3 class="col-lg-6">Api 文档<small title="版本号"></small></h3>
                <div class="col-lg-6 d-flex align-items-center justify-content-end">
                    <div class="mr-2">
                        <a href="#" id="btnCustomDebug">[自定义调试]</a>
                        <a href="#" id="btnDataGen">[新增接口]</a>
                    </div>
                    <div>
                        <span id="projectTitle"></span>
                        服务器：
                        <select style="padding:2px 0" id="sltServer">
                            <option value="http://www.baidu.com">远程</option>
                            <option data-url="http://127.0.0.1:8080">本地</option>
                        </select>
                        <input type="text" id="txtServerUrl" value="" size="25">
                    </div>
                </div>
            </div>
        </header>
        <div class="row">
            <aside class="col-lg-2">
                <ul class="list-group">
                    <li><a href="#" title="">接口示例</a></li>
                </ul>
            </aside>
            <main class="col-lg-10">
                <h4>欢迎使用！</h4>
                <p>点击侧边栏菜单查看接口详情。</p>
            </main>
        </div>
    </div>
    <!-- 自定义调试 -->
    <template id="customDebug">
        <section id="customDebugSection">
            <h4>自定义接口调试</h4>
            <div class="my-2 text-muted">前有 Postman，后有 <a href="https://github.com/liyasthomas/postwoman"
                    target="_blank">Postwoman</a>，这个就叫 PostMe！<br />如果本工具不能满足需求，推荐使用无广告、无恶意代码的 <a
                    href="http://tool.chinaz.com/tools/httptest.aspx?jdfwkey=2ugnf2" target="_blank">站长工具-HTTP接口测试工具</a>
                或 <a href="https://www.sojson.com/http/test.html" target="_blank">SOJSON-HTTP模拟请求。</a></div>

            <h6>接口地址：</h6>
            <form name="url_form">
                <div class="input-group input-group-sm mb-3">
                    <div class="input-group-prepend">
                        <select name="method" title="请求方式" required>
                            <option value="GET">GET</option>
                            <option value="POST">POST</option>
                            <option value="PUT">PUT</option>
                            <option value="DELETE">DELETE</option>
                            <option value="HEADER">HEADER</option>
                        </select>
                    </div>
                    <input name="url" type="text" class="form-control" placeholder="请填写接口 URL（不含 QueryString 参数）"
                        title="接口 URL" required>
                    <div class="input-group-prepend">
                        <button class="btn btn-secondary btn-debug" type="button">发送请求</button>
                    </div>
                </div>
            </form>
            <form name="querystring_form">
                <div>
                    <h6>QueryString 参数：</h6>
                    <textarea name="querystring" class="form-control"
                        placeholder="QueryString 参数。示例：id=1&ts=12330202&sign=slfj23lkjdsf09324ldf0732a"></textarea>
                </div>
            </form>
            <div class="my-3">
                <h6>Body 参数：</h6>
                <form name="body_form">
                    <table class="table table-sm table-striped table-hover">
                        <thead>
                            <tr>
                                <th>参数名称</th>
                                <th>参数值</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td>
                                    <input class="form-control form-control-sm" type="text" name="key"
                                        placeholder="参数名称" />
                                </td>
                                <td>
                                    <input class="form-control form-control-sm" type="text" name="value"
                                        placeholder="参数值" />
                                </td>
                                <td class="cell-operate">
                                    <button class="btn btn-sm btn-secondary btn-add-row mr-1">添加</button>
                                    <button class="btn btn-sm btn-secondary btn-remove-row">删除</button>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </form>
            </div>
            <div class="my-3">
                <h6>Header 参数：</h6>
                <form name="header_form">
                    <table class="table table-sm table-striped table-hover">
                        <thead>
                            <tr>
                                <th>参数名称</th>
                                <th>参数值</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td>
                                    <input class="form-control form-control-sm" type="text" name="key"
                                        value="Authorization" placeholder="参数名称" />
                                </td>
                                <td>
                                    <input class="form-control form-control-sm" type="text" name="value"
                                        placeholder="参数值"
                                        value="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvd3d3LnBvd2Vyc2RrLmNuIiwiYXVkIjoiIiwiaWF0IjoxNjA5ODI3OTk1LCJuYmYiOjE2MDk4NjM5OTUsInVzZXJfaWQiOjF9.0FvKebQPEqXYvRHYFJtHCJF7AZdtHifDmMn1w0cM-2k" />
                                </td>
                                <td class="cell-operate">
                                    <button class="btn btn-sm btn-secondary btn-add-row mr-1">添加</button>
                                    <button class="btn btn-sm btn-secondary btn-remove-row">删除</button>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </form>
            </div>
            <div>
                <h6>响应消息：</h6>
                返回值 Response Text 信息（<a href="https://www.json.cn/" target="_blank">JSON在线解析及格式化验证</a>）
                <div class="url-info"></div>
                <textarea name="response_text" class="form-control form-control-sm" rows="5"
                    placeholder="请点击左上角的【发送请求】按钮获取实时响应消息"></textarea>
                返回 Response Headers 信息
                <textarea name="response_headers" class="form-control form-control-sm" rows="4"
                    placeholder="请点击左上角的【发送请求】按钮获取实时响应消息"></textarea>
            </div>
        </section>

    </template>
    <!-- 代码生成器 -->
    <template id="dataGen">
        <h4>欢迎使用！</h4>
        <div class="d-flex">
            <div class="card mt-2" style="width:600px">
                <div class="card-header">代码生成器</div>
                <div class="card-body">
                    <form name="datagen_form">
                        <div class="form-group row">
                            <label class="col-sm-2 col-form-label">标题</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control form-control-sm" name="title" placeholder="标题"
                                    required>
                            </div>
                        </div>
                        <div class="form-group row">
                            <label class="col-sm-2 col-form-label">简介</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control form-control-sm" name="intro" placeholder="简介">
                            </div>
                        </div>
                        <div class="form-group row">
                            <label class="col-sm-2 col-form-label">请求方式</label>
                            <div class="col-sm-10">
                                <select name="method">
                                    <option value="GET">GET</option>
                                    <option value="POST">POST</option>
                                    <option value="PUT">PUT</option>
                                    <option value="DELETE">DELETE</option>
                                    <option value="HEADER">HEADER</option>
                                </select>
                            </div>
                        </div>
                        <div class="form-group row">
                            <label class="col-sm-2 col-form-label">URL</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control form-control-sm" name="url" placeholder="URL"
                                    required>
                            </div>
                        </div>
                        <div class="form-group">
                            <label>querystring 参数 <a href="#" class="btn-add-param">+</a></label>
                            <textarea class="form-control form-control-sm" name="querystringParams" rows="3"
                                placeholder="">{name: '', required: true, dataType: 'string', comment: '', default: ''},</textarea>
                        </div>
                        <div class="form-group">
                            <label>bodyParams 参数 <a href="#" class="btn-add-param">+</a></label>
                            <textarea class="form-control" name="bodyParams" rows="5"
                                placeholder="">{name: '', required: true, dataType: 'string', comment: '', default: ''},</textarea>
                        </div>
                        <div class="form-group">
                            <label>headersParams 参数 <a href="#" class="btn-add-param">+</a></label>
                            <textarea class="form-control form-control-sm" name="headersParams" rows="2"
                                placeholder="">{name: '', required: true, dataType: 'string', comment: '', default: ''},</textarea>
                        </div>
                        <div class="form-group">
                            <label>成功响应消息示例</label>
                            <textarea class="form-control form-control-sm" name="success"
                                placeholder="">{status:1, msg:"", data:null, code:0}</textarea>
                        </div>
                        <div class="form-group">
                            <label>错误响应消息示例</label>
                            <textarea class="form-control form-control-sm" name="error"
                                placeholder="">{status:0, msg:"错误消息", data:null, code:0}</textarea>
                        </div>
                        <button type="submit" class="btn btn-primary">保存</button>
                    </form>
                </div>
            </div>
            <!-- /代码生成器 -->
            <div class="flex-grow-1 ml-2">
                <div><strong>JSON 代码：</strong>
                    （将代码复制到 apis 里面）
                    <a href="#" class="btnCut" data-target="#jsonCode">剪切</a>
                </div>
                <textarea class="form-control" id="jsonCode" placeholder="JSON 代码"></textarea>
            </div>
        </div>
    </template>
</body>
<script>
    // ============= 功能函数库 =============
    // 页面初始化
    function pageInit() {
        // 获取所有项目配置数据
        let projectInfo = getProjectConfig(projId)
        if (!projectInfo) {
            alert('项目 ' + (projId || '[默认项目]') + ' 不存在')
            return;
        }
        projectInfo.alias = projectInfo.alias || projectInfo.title;
        // 初始化
        initAside(projectInfo.api_data || []);
        initHeader(projectInfo);
        initVersion();
    }
    // 初始化版本号
    function initVersion() {
        let logs = [
            '1.0.0 postman 太重，其他在线接口调试工具功能过于简陋，因此决定自己开发一个可以本地断网使用的，支持接口收藏的轻量的在线调试工具',
            '1.1.0 由 api-doc 重构为 postme',
            '1.1.2 侧边栏支持目录功能',
            '1.1.3 预设接口调试界面的请求类型由只读文本框改为下拉框，以便临时更换请求类型。表单输入框支持自动识别数据类型。',
        ]
        let elNode = document.querySelector('header h3 small');
        elNode.innerText = '-' + VERSION;
        elNode.title = logs.join("\n");
    }
    // 头部初始化
    function initHeader(projectInfo) {
        document.querySelector('#btnCustomDebug').addEventListener('click', function (evt) {
            document.querySelector('main').innerHTML = document.querySelector('#customDebug').innerHTML;
            // 防止重复实例化
            if (!evt.target.dataset.isInit) {
                initCustomDebugForm();
                evt.target.dataset.isInit = 1;
            }
        })
        // 新增接口（生成数据结构）
        document.querySelector('#btnDataGen').addEventListener('click', function (evt) {
            document.querySelector('main').innerHTML = document.querySelector('#dataGen').innerHTML;
            // 防止重复初始化
            if (!evt.target.dataset.isInit) {
                initCodeForm();
                evt.target.dataset.isInit = 1;
            }
        })
        // 项目下拉框
        let html = '<select id="sltProject" onchange="location.href=\'?proj=\'+this.value">';
        getProjectConfig('ALL').forEach(item => {
            html += '<option value="' + item.name + '"' + (item.name == projId ? ' selected' : '') + '>' + item.text + '</option>';
        })
        html += '</select>';
        document.querySelector('#projectTitle').innerHTML = html;
        console.log('==', document.location.origin);
        // 服务器下拉框
        let elServerSelect = document.querySelector('#sltServer');
        elServerSelect.setAttribute('title', '[项目] ' + projectInfo.alias);
        html = '';
        projectInfo.servers.forEach(servItem => {
            // 支持 ~/index.php 这种写法，符号 ~ 会自动替换为 http://xxx.com
            if (servItem.url.indexOf('~') == 0) {
                servItem.url = document.location.origin + servItem.url.substring(1);
            }
            html += `<option value="${servItem.url}"${servItem.active ? ' selected' : ''}>${servItem.text}</option>`;
        })
        elServerSelect.innerHTML = html;
        if (elServerSelect.selectedIndex > -1) {
            elServerSelect.nextElementSibling.value = elServerSelect.options[elServerSelect.selectedIndex].value;
        }
        elServerSelect.addEventListener('change', function (evt) {
            elServerSelect.nextElementSibling.value = elServerSelect.options[elServerSelect.selectedIndex].value;
        })
    }
    // 侧边栏初始化
    function initAside(apis) {
        // 角标过期时间（24小时前的毫秒数）
        let timestampOfBadge = new Date().getTime() - 24 * 3600 * 1000;
        let asideHtml = '', badgeHtml = '', n = 0, apiData = [];
        makeHtml(apis);
        function makeHtml(apis) {
            for (let item of apis) {
                if (item.isFolder) {
                    asideHtml += '<li><span class="d-flex"><a href="#" class="aside-folder-switcher">-</a><strong>' + item.title + '</strong></span><ul>';
                    makeHtml(item.api_data);
                    asideHtml += '</ul></li>';
                    continue;
                }
                // 接口不可用则跳过
                if (false === item.enable) {
                    n++;
                    continue;
                }
                badgeHtml = '';
                if (item.created_at && (new Date(item.created_at).getTime() > timestampOfBadge)) {
                    // 24小时内创建的接口的显示角标
                    badgeHtml = `<span class="icon" title="创建于 ${item.created_at}">+</span>`;
                }
                if (item.updated_at && (new Date(item.updated_at).getTime() > timestampOfBadge)) {
                    // 24小时内更新的接口的显示角标
                    badgeHtml = `<span class="icon" title="更新于 ${item.updated_at}">M</span>`;
                }
                n++;
                apiData.push(item);
                asideHtml += `<li class="d-flex justify-content-between"><a class="link" href="#" data-id="${n}" title="[${item['method']}] ${item['title']}">${n}. ${item['title']}</a>${badgeHtml}</li>`;
            }
        }
        apis = null;
        document.querySelector('aside ul').innerHTML = asideHtml;
        document.querySelectorAll('aside a').forEach(el => {
            el.addEventListener('click', function (evt) {
                evt.preventDefault();
                loadContent(apiData[evt.target.dataset.id - 1])
            })
        })
        document.querySelectorAll('aside .aside-folder-switcher').forEach(el => {
            el.addEventListener('click', function (evt) {
                evt.preventDefault();
                let elTarget = evt.target;
                elTarget.innerText = (elTarget.innerText == '-') ? '+' : '-';
                elTarget.parentNode.nextElementSibling.classList.toggle('d-none');
            })
        })
    }
    // 生成参数签名
    function makeSign(params, appid = -1, appkey = 'a99AE2d2a736X65f5Ye63Ae299b0e339') {
        // 加入时间戳参数
        if (!params['timestamp']) {
            params['timestamp'] = new Date().getTime();
        }
        // 加入应用ID参数
        params['appid'] = appid;
        // 加入应用Key参数
        params['appkey'] = appkey;
        // 加入随机值参数
        if (!params['nonce']) {
            params['nonce'] = getRandomString();
        }
        // 数组按键值正序重新排序
        params = objKeySort(params);

        // 按参数名称从小到大的顺序重新排列
        var query = 'appid=' + appid + '&appkey=' + appkey + '&nonce=' + params['nonce'] + '&timestamp=' + params['timestamp'];
        // 用md5加密重新串联成请求字符串，截取中间16位作为签名
        var sign = md5(query).substring(8, 24);
        return sign;
        // // 用md5加密重新串联成请求字符串的参数数组
        // $sign = md5(http_build_query($params));
        // // 截取中间16位作为签名
        // $sign = substr($sign, 8, 16);
        // // 删除appkey参数
        // unset($params['appkey']);
        // // 加入签名参数
        // $params['sign'] = $sign;
        // return $sign;
    }
    // 把对象按照属性名的字母顺序进行排列
    function objKeySort(obj, isReverse = false) {
        var newKeys = Object.keys(obj).sort();
        if (isReverse) {
            newKeys = newKeys.reverse();
        }
        var newObj = {};//创建一个新的对象，用于存放排好序的键值对
        newKeys.forEach(k => {
            newObj[k] = obj[k];//向新创建的对象中按照排好的顺序依次增加键值对
        })
        return newObj;//返回排好序的新对象
    }
    // 获取上级节点
    function getParentNode(elNode, nodeName, n = 5) {
        let elParent = elNode;
        while (n > 0) {
            if (elParent.nodeName == nodeName) {
                return elParent;
            }
            elParent = elParent.parentNode;
            n--;
        }
        return elParent;
    }
    // 返回随机字符串
    function getRandomString(len = 6) {
        return Math.random().toString(36).substr(2, len);
    }
    // 获取时间戳
    function getTimestamp(isMicrotime = false) {
        return isMicrotime ? new Date().getTime() : Math.round(new Date().getTime() / 1000);
    }
    // 参数的数据类型转换成表单控件类型
    function getInputElement(param) {
        let inputType = '';
        if (param.inputType) {
            // 如果参数有设置输入控件类型，则直接读取参数设置的控件类型值
            inputType = param.inputType;
        } else {
            // 自动把数据类型转换成表单控件类型
            switch (param.dataType) {
                case 'date':
                    inputType = 'date';
                    break;
                case 'int':
                    inputType = 'number';
                    break;
                default:
                    inputType = 'search';
                    break;
            }
        }
        // text 控件
        let html = '';
        switch (inputType) {
            case 'textarea':
                html = `<textarea class="border w-100" name="${param.name}" placeholder="${param.comment}" title="${param.comment}" ${param.required ? ' required' : ''}>${param.default}</textarea>`;
                break;
            default:
                let valueInputHtml = (param.default && param.default.indexOf('"') > -1) ? `value='${param.default}'` : `value="${param.default || ''}"`;
                html = `<input class="border" type="${inputType}" name="${param.name}" placeholder="${param.comment}" title="${param.comment}" ${valueInputHtml}${param.required ? ' required' : ''} />`;
                break;
        }
        if (inputType == 'password') {
            html += '<button class="btn-show-password border" title="显示密码">显示</button>';
        }
        if (param.dataType == 'timestamp') {
            html += '<button class="btn-refresh-timestamp border" title="刷新时间戳">刷新</button>'
        }
        switch (param.name) {
            case 'sign':
                html += '<button class="btn-sign border" title="生成签名字符串">生成签名</button>'
                break;
            case 'nonce':
                html += '<button class="btn-nonce border" title="生成随机字符串">生成随机字符串</button>'
                break;
            default:
                break;
        }
        // select 控件
        if (param.pickerData) {
            html += `<select class="border ml-1" onchange="selectPickerChange(this)" title="快速选择器">`;
            for (let kv of param.pickerData) {
                html += `<option value="${kv.value}"${(kv.actived || (param.default == kv.value)) ? ' selected' : ''}>${kv.text}</option>`;
            }
            html + '</select>';
        }
        return html;
    }
    // 加载主体内容
    function loadContent(data) {
        if (!data) {
            return;
        }
        let querystringParamsHtml = '', bodyParamsHtml = '', headerParamsHtml = '', inputControlHtml = '';
        if (data.querystringParams) {
            querystringParamsHtml = '';
            for (let param of data.querystringParams) {
                if (param.default) {
                    // 变量替换，支持 {time},{microtime}
                    param.default = param.default.replace('{microtime}', new Date().getTime()).replace('{time}', Math.round(new Date().getTime() / 1000))
                }
                inputControlHtml = getInputElement(param);
                querystringParamsHtml += `<tr>
                <td>${param.name}</td>
                <td>${param.dataType || 'string'}</td>
                <td>${param.required ? '<span class="text-danger">是</span>' : '<span class="text-muted">否</span>'}</td>
                <td>${param.comment || ''}</td>
                <td>${inputControlHtml}</td>
            </tr>`;
            }
        }
        if (data.bodyParams) {
            bodyParamsHtml = '';
            for (let param of data.bodyParams) {
                if (param.default) {
                    // 变量替换，支持 {time},{microtime}
                    param.default = param.default.replace('{microtime}', new Date().getTime()).replace('{time}', Math.round(new Date().getTime() / 1000))
                }
                inputControlHtml = getInputElement(param);
                bodyParamsHtml += `<tr>
                <td>${param.name}</td>
                <td>${param.dataType || 'string'}</td>
                <td>${param.required ? '<span class="text-danger">是</span>' : '<span class="text-muted">否</span>'}</td>
                <td>${param.comment || ''}</td>
                <td>${inputControlHtml}</td>
            </tr>`;
            }
        }
        if (data.headersParams) {
            headerParamsHtml = '';
            for (let param of data.headersParams) {
                if (param.default) {
                    // 变量替换，支持 {time},{microtime}
                    param.default = param.default.replace('{microtime}', new Date().getTime()).replace('{time}', Math.round(new Date().getTime() / 1000))
                }
                inputControlHtml = getInputElement(param);
                headerParamsHtml += `<tr>
                <td>${param.name}</td>
                <td>${param.dataType || 'string'}</td>
                <td>${param.required ? '<span class="text-danger">是</span>' : '<span class="text-muted">否</span>'}</td>
                <td>${param.comment || ''}</td>
                <td>${inputControlHtml}</td>
            </tr>`;
            }
        }
        let html = `<section>
                <h4>${data.title}</h4>
                <div class="my-2 text-muted">${data.intro || ''}</div>
                <h6>接口地址：</h6>
                <form name="url_form">
                <div class="input-group input-group-sm mb-3">
                    <div class="input-group-prepend">
                        <select name="method" title="请求方式" required>
                            <option value="GET"${data.method == 'GET' ? ' selected' : ''}>GET</option>
                            <option value="POST"${data.method == 'POST' ? ' selected' : ''}>POST</option>
                            <option value="PUT"${data.method == 'PUT' ? ' selected' : ''}>PUT</option>
                            <option value="DELETE"${data.method == 'DELETE' ? ' selected' : ''}>DELETE</option>
                            <option value="HEADER"${data.method == 'HEADER' ? ' selected' : ''}>HEADER</option>
                        </select>
                    </div>
                    <input name="url" type="text" class="form-control" placeholder="URL" value="${data.url}" required>
                    <div class="input-group-prepend">
                        <button class="btn btn-secondary btn-debug" type="button">发送请求</button>
                    </div>
                </div>
                </form>
                <div>
                    <h6>QueryString 参数：</h6>
                    <form name="querystring_form">
                    <table class="table table-sm table-striped table-hover">
                        <thead>
                        <tr>
                            <th>名称</th>
                            <th>类型</th>
                            <th>必填</th>
                            <th>描述</th>
                            <th>默认值</th>
                        </tr>
                        </thead>
                        <tbody>
                        ${querystringParamsHtml}
                        </tbody>
                    </table>
                    </form>
                </div>
                <div>
                    <h6>Body 参数<small>（仅 POST、PUT、DELETE 请求方式有效）</small>：</h6>
                    <form name="body_form">
                    <table class="table table-sm table-striped table-hover">
                        <thead>
                        <tr>
                            <th>名称</th>
                            <th>类型</th>
                            <th>必填</th>
                            <th>描述</th>
                            <th>默认值</th>
                        </tr>
                        </thead>
                        <tbody>
                        ${bodyParamsHtml}
                        </tbody>
                    </table>
                    </form>
                </div>
                <div>
                    <h6>Header 参数：</h6>
                    <form name="header_form">
                    <table class="table table-sm table-striped table-hover">
                        <thead>
                        <tr>
                            <th>名称</th>
                            <th>类型</th>
                            <th>必填</th>
                            <th>描述</th>
                            <th>默认值</th>
                        </tr>
                        </thead>
                        <tbody>
                        ${headerParamsHtml}
                        </tbody>
                    </table>
                    </form>
                </div>
                <div>
                    <div><h6>成功响应消息示例：</h6>
                        <code>${data.result.success}</code>
                    </div>
                    <div class="my-2"><h6>错误响应消息示例：</h6>
                        <code>${data.result.error}</code>
                    </div>
                    <div>
                        <h6>响应消息：</h6>
                        返回值 Response Text 信息（<a href="https://www.json.cn/" target="_blank">JSON在线解析及格式化验证</a>）
                        <div class="url-info text-info"></div>
                        <textarea name="response_text" class="form-control form-control-sm" rows="5"
                            placeholder="请点击左上角的【发送请求】按钮获取实时响应消息"></textarea>
                        返回 Response Headers 信息
                        <textarea name="response_headers" class="form-control form-control-sm" rows="4"
                            placeholder="请点击左上角的【发送请求】按钮获取实时响应消息"></textarea>
                    </div>
                </div>
            </section>`;
        document.querySelector('main').innerHTML = html;
        document.querySelector('main').addEventListener('click', function (evt) {
            evt.preventDefault();
            if (evt.target.tagName == 'BUTTON') {
                // 生成签名字符串
                if (evt.target.classList.contains('btn-sign')) {
                    if (typeof (data.signHandler) == 'function') {
                        data.signHandler(getParentNode(evt.target, 'TABLE'));
                    }
                    return;
                }
                // 生成随机字符串
                if (evt.target.classList.contains('btn-nonce')) {
                    evt.target.previousElementSibling.value = getRandomString();
                    return;
                }
                // 刷新时间戳
                if (evt.target.classList.contains('btn-refresh-timestamp')) {
                    evt.target.previousElementSibling.value = getTimestamp();
                    return;
                }
                // 显示密码
                if (evt.target.classList.contains('btn-show-password')) {
                    evt.target.previousElementSibling.type = evt.target.previousElementSibling.type == 'text' ? 'password' : 'text';
                    return;
                }
            }
        })
        // 预设API数据 - 事件处理
        document.querySelector('.btn-debug').addEventListener('click', function (evt) {
            evt.preventDefault();
            // 获取大容器
            let elSection = evt.target;
            for (let i = 0; i < 10; i++) {
                if (elSection.tagName == 'SECTION') {
                    break;
                }
                elSection = elSection.parentNode;
            }
            // 实例化表单
            let urlForm = elSection.querySelector('form[name="url_form"]'),
                querystringForm = elSection.querySelector('form[name="querystring_form"]'),
                bodyForm = elSection.querySelector('form[name="body_form"]'),
                headerForm = elSection.querySelector('form[name="header_form"]');
            // 表单验证
            // 狗日的，终于找到显示的方法了！还是 MDN 文档比较齐全！input.checkValidity() 浏览器尚未支持，只能用 input.validity.valid。
            // 示例：elInput.setCustomValidity(''); if (elInput.validity.valid === false) { elInput.setCustomValidity(elInput.validationMessage);}
            if (!urlForm.checkValidity()) {
                urlForm.reportValidity();
                return;
            }
            if (!querystringForm.checkValidity()) {
                querystringForm.reportValidity();
                return;
            }
            if (!bodyForm.checkValidity()) {
                bodyForm.reportValidity();
                return;
            }
            if (!headerForm.checkValidity()) {
                headerForm.reportValidity();
                return;
            }
            // 获取 querystring 参数
            let urlSearchParams = new URLSearchParams();
            new FormData(querystringForm).forEach((v, k) => {
                urlSearchParams.set(k, v);
            })
            // 获取 headers 参数
            let headerParams = {};
            new FormData(headerForm).forEach((v, k) => {
                headerParams[k] = v;
            })
            // 发送请求
            requestHandler(urlForm.elements['method'].value, urlForm.elements['url'].value, urlSearchParams.toString(), elSection, headerParams, new FormData(bodyForm));
        })
    }
    // 代码生成器表单初始化
    function initCodeForm() {
        console.log('initCodeForm');
        let elForm = document.querySelector('form[name="datagen_form"]');
        // 复制按钮
        elForm.querySelectorAll('.btnCopy').forEach(el => {
            el.addEventListener('click', function (evt) {
                let elInput = document.querySelector(el.dataset.target);
                elInput.select();
                document.execCommand('copy');
            })
        })
        // 剪切按钮
        elForm.querySelectorAll('.btnCut').forEach(el => {
            el.addEventListener('click', function (evt) {
                let elInput = document.querySelector(el.dataset.target);
                elInput.select();
                document.execCommand('cut');
            })
        })
        // 新增参数按钮
        elForm.querySelectorAll('.btn-add-param').forEach(el => {
            el.addEventListener('click', function (evt) {
                evt.preventDefault();
                evt.target.parentNode.nextElementSibling.value += "\r{name: '', required: true, dataType: 'string', comment: '', default: ''},";
            })
        })
        // 提交表单按钮
        elForm.querySelector('button[type="submit"]').addEventListener('click', function (evt) {
            evt.preventDefault();
            let content = '', resultContent = '', tab = '    ', datetime = new Date().toLocaleDateString().replace(/\//g, '-');
            let elForm = document.querySelector('form');
            let formData = new FormData(elForm);
            for (let kv of formData.entries()) {
                switch (kv[0]) {
                    case 'querystringParams':
                    case 'bodyParams':
                    case 'headersParams':
                        kv[1] = kv[1].replace(/\{name/g, tab + tab + '{name');
                        content += `${tab}${kv[0]}:[\r${kv[1]}\r${tab}],\r`;
                        break;
                    case 'success':
                        resultContent += `${tab}${tab}${kv[0]}:'${kv[1]}',\r`;
                        break;
                    case 'error':
                        resultContent += `${tab}${tab}${kv[0]}:'${kv[1]}',\r`;
                        break;
                    default:
                        content += `${tab}${kv[0]}:'${kv[1]}',\r`;
                        break;
                }
            }
            content += `${tab}result:{\r${resultContent}${tab}},\r`;
            content += `${tab}created_at: '${datetime}',\r`;
            document.querySelector('#jsonCode').value += '{\r' + content + '},\r';
            elForm.reset();
        })
    }
    // 自定义接口调试表单初始化
    function initCustomDebugForm() {
        let elSection = document.querySelector('#customDebugSection');
        if (!elSection) {
            return;
        }
        console.log('initCustomDebugForm');
        // 添加行事件
        function addRowEvent(evt) {
            evt.preventDefault();
            console.log('add');
            let elTrow = evt.target.parentNode.parentNode;
            let elTr = document.createElement('TR');
            elTr.innerHTML = elTrow.innerHTML;
            if (elTrow.nextElementSibling) {
                elTrow.parentNode.insertBefore(elTr, elTrow.nextElementSibling);
            } else {
                elTrow.parentNode.appendChild(elTr);
            }
            elTr.querySelector('button.btn-add-row').addEventListener('click', addRowEvent);
            elTr.querySelector('button.btn-remove-row').addEventListener('click', removeRowEvent);

        }
        // 删除行事件
        function removeRowEvent(evt) {
            evt.preventDefault();
            console.log('remove');
            let elTrow = evt.target.parentNode.parentNode;
            // 至少保留一行
            if (elTrow.parentNode.querySelectorAll('tr').length > 1) {
                elTrow.parentNode.removeChild(elTrow);
            }
        }
        // 添加行按钮点击事件
        elSection.querySelectorAll('button.btn-add-row').forEach(el => {
            el.addEventListener('click', addRowEvent)

        })
        // 删除行按钮点击事件
        elSection.querySelectorAll('button.btn-remove-row').forEach(el => {
            el.addEventListener('click', removeRowEvent)
        })
        // 获取 二维的 [名称,值] 数据表格的参数表单的数据
        function getParamsFormData(elForm) {
            let data = {};
            if (!elForm) {
                return data;
            }
            let keys = [], values = [];
            let formData = new FormData(elForm);
            formData.forEach((v, k) => {
                if (k == 'key') {
                    keys.push(v.trim());
                }
                if (k == 'value') {
                    values.push(v.trim());
                }
            })
            for (let i in keys) {
                if (keys[i].length > 0) {
                    data[keys[i]] = values[i];
                }
            }
            return data;
        }
        // 自定义调试 - 发送请求按钮点击事件
        elSection.querySelector('.btn-debug').addEventListener('click', function (evt) {
            evt.preventDefault();
            let urlForm = elSection.querySelector('form[name="url_form"]'),
                querystringForm = elSection.querySelector('form[name="querystring_form"]'),
                bodyParams = getParamsFormData(elSection.querySelector('form[name="body_form"]')),
                headerParams = getParamsFormData(elSection.querySelector('form[name="header_form"]'));
            let errors = [];
            if (!urlForm.checkValidity()) {
                urlForm.reportValidity();
                return;
            }
            let querystring = querystringForm.elements['querystring'].value;
            let bodyFormData = new FormData();
            if (bodyParams) {
                for (let k in bodyParams) {
                    bodyFormData.set(k, bodyParams[k]);
                }
            }
            requestHandler(urlForm.elements['method'].value, urlForm.elements['url'].value, querystring, elSection, headerParams, bodyFormData);
        })
    }

    // 执行远程数据请求
    function requestHandler(method, url, querystring, elSection, headerParams = null, bodyFormData = null) {
        url = url.trim();
        method = method.toUpperCase();
        let elResponseText = elSection.querySelector('textarea[name="response_text"]'),
            elResponseHeaders = elSection.querySelector('textarea[name="response_headers"]');
        let servUrl = document.querySelector('#txtServerUrl').value;
        url = url.indexOf('http') === 0 ? url : (servUrl.replace(/\/$/, '') + url);
        // 模拟提交表单
        let fetchInit = {
            method: method,
            mode: 'cors',
            credentials: 'include'
        }
        if (headerParams) {
            fetchInit.headers = {};
            for (let k in headerParams) {
                fetchInit.headers[k] = headerParams[k];
            }
        }
        if (method == 'GET') {
            bodyFormData.forEach((v, k) => {
                querystring += `&${k}=${v}`
            })
            if (querystring.length > 0) {
                url += (url.indexOf('?') > -1 ? '&' : '?') + (querystring.indexOf('&') == 0 ? querystring.substring(1) : querystring);
            }
        } else {
            fetchInit.body = bodyFormData;
        }
        let elUrlInfo = document.querySelector('.url-info');
        if (elUrlInfo) {
            elUrlInfo.innerText = url;
        }
        console.log('fetchInit', fetchInit);
        let statusMessage = { 404: '请求地址不存在 [404]', 500: '服务器代码错误 [500]' }
        try {
            fetch(url, fetchInit)
                .then(res => {
                    console.log('[fetch-then] res=', res);
                    elResponseHeaders.value = JSON.stringify(res.headers);
                    if (res.status == 200) {
                        return res.json();
                    }
                    throw new Error(JSON.stringify({ status: res.status, message: res.statusText }));
                })
                .then(data => elResponseText.value = JSON.stringify(data))
                .catch(err => {
                    console.log('[fetch-catch] err=', typeof (err.message), err.message);
                    let message = '';
                    try {
                        let msgInfo = JSON.parse(err.message);
                        message = statusMessage[msgInfo.status] || `网络请求错误 [${msgInfo.status}]`;
                        message += '：' + msgInfo.message;
                    } catch (error) {
                        message = '网络请求错误：' + err.message;
                    }
                    elResponseText.value = message + '（请打开浏览器控制台查看更多信息）'
                });

        } catch (error) {
            console.log('[try-cache] error=', error);
            elResponseText.value = '网络异常：' + error.message;
        }
    }
    // 选择器切换后，自动把值填充到前面的input控件中
    function selectPickerChange(elSelect) {
        elSelect.previousElementSibling.value = elSelect.options[elSelect.selectedIndex].value;
    }
</script>

<script>
    // 示例数据库（演示用）
    function getDemoProject() {
        // 预定义通用参数
        let param_header_token = {
            name: 'Authorization',
            required: true,
            dataType: 'string',
            comment: '登录认证头部信息',
            default: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvd3d3LnBvd2Vyc2RrLmNuIiwiYXVkIjoiIiwiaWF0IjoxNjAyNTg4MjIzLCJuYmYiOjE2MDI2MjQyMjMsInVzZXJfaWQiOjF9.ZkFUaD3Kgl_bQAEkBoYDJ31PQsXRPHa1gBeJWottMYY',
            pickerData: [{
                value: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvd3d3LnBvd2Vyc2RrLmNuIiwiYXVkIjoiIiwiaWF0IjoxNjAyNTg4MjIzLCJuYmYiOjE2MDI2MjQyMjMsInVzZXJfaWQiOjF9.ZkFUaD3Kgl_bQAEkBoYDJ31PQsXRPHa1gBeJWottMYY',
                text: 'user-1'
            }, {
                value: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvbWFsbDIucHMiLCJhdWQiOiIiLCJpYXQiOjE2MTkwODE5NzQsIm5iZiI6MTYxOTExNzk3NCwidXNlcl9pZCI6Mn0.BHVPMR_pD57vCKmjYvE_yQE8sHbJm5vybGRgsVfRHp0',
                text: 'user-2'
            }, {
                value: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvbWFsbDIucHMiLCJhdWQiOiIiLCJpYXQiOjE2MTkwODIwNDUsIm5iZiI6MTYxOTExODA0NSwidXNlcl9pZCI6M30.e85J_n-KuSkVFSfbYLnxY93__dqEYihfnzf0oAvKvE0',
                text: 'user-3'
            }, {
                value: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvbWFsbDIucHMiLCJhdWQiOiIiLCJpYXQiOjE2MTkwODIwNzcsIm5iZiI6MTYxOTExODA3NywidXNlcl9pZCI6NH0.NAXrHkH_clbbUalHrYDNCMZ7dK_PXAaNhHZlEtZKZkg',
                text: 'user-4'
            }, {
                value: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCIwIjoiaHR0cDpcL1wvbWFsbDIucHMiLCJhdWQiOiIiLCJpYXQiOjE2MTkwODIwOTMsIm5iZiI6MTYxOTExODA5MywidXNlcl9pZCI6NX0.cRCcNeQNn2q-3i3USh7JIDggGBcmoMOLcULjIOJc0Gc',
                text: 'user-5'
            }]
        },
            param_page = {
                name: 'page',
                required: true,
                dataType: 'int',
                comment: '页码',
                default: '1'
            },
            param_page_size = {
                name: 'page_size',
                required: true,
                dataType: 'int',
                comment: '分页尺寸',
                default: '10'
            },
            param_id = {
                name: 'id',
                required: true,
                dataType: 'int',
                comment: 'ID',
                default: '1'
            },
            param_ids = {
                name: 'id',
                required: true,
                dataType: 'string',
                comment: '编号列表，以英文逗号隔开',
                default: '1,2,3'
            },
            param_is_debug = {
                name: 'is_debug',
                required: true,
                dataType: 'int',
                comment: '是否调试模式',
                default: '1',
                pickerData: [{
                    value: '1',
                    text: '是'
                }, {
                    value: '0',
                    text: '否'
                },]
            },
            param_timestamp = {
                name: 'timestamp',
                required: true,
                dataType: 'timestamp', // timestamp类型会出现刷新按钮
                comment: '时间戳',
                default: '{time}' // 变量替换，支持 {time},{microtime}
            },
            param_appid = {
                name: 'appid',
                required: true,
                dataType: 'string',
                comment: '应用唯一标识',
                default: 'test'
            },
            param_appkey = {
                name: 'appkey',
                required: true,
                dataType: 'string',
                comment: '应用签名密钥',
                default: 'a99AE2d2a736X65f5Ye63Ae299b0e339'
            },
            param_nonce = {
                name: 'nonce',
                required: true,
                dataType: 'string',
                comment: '随机生成的字符串',
                default: 'a20d6p'
            },
            param_sign = {
                name: 'sign',
                required: true,
                dataType: 'string',
                comment: '参数签名',
                default: '',
            };
        // 返回项目接口数据
        return {
            // 是否可用
            enable: true,
            // 项目名称
            title: '演示',
            // 项目别称（优先显示）
            alias: '演示项目',
            // 服务器基础地址配置
            servers: [
                { text: '远程', url: 'http://www.baidu.com' },
                // 支持 ~/index.php 占位符写法，第一个 ~ 符号会自动替换为 document.location.origin
                { text: '本地', url: '~', active: true },
            ],
            // 接口数据配置
            api_data: [
                // 示例接口
                {
                    title: '演示接口',
                    intro: '接口的描述内容',
                    created_at: '2020-12-31',
                    updated_at: new Date().toLocaleDateString(),
                    enable: true,
                    method: 'GET',
                    url: '/index.php/api/login',
                    querystringParams: [
                        {
                            name: 'password',
                            required: true,
                            dataType: 'string',
                            inputType: 'password',
                            comment: '密码',
                            default: 'pwd123456'
                        },
                        param_timestamp,
                        {
                            name: 'gender',
                            required: true,
                            dataType: 'int',
                            comment: '性别',
                            default: '1',
                            pickerData: [{
                                value: '0',
                                text: '女'
                            }, {
                                value: '1',
                                text: '男'
                            }, {
                                value: '2',
                                text: '保密'
                            }],
                        }, {
                            name: 'extra',
                            required: true,
                            dataType: 'string',
                            inputType: 'textarea',
                            comment: '扩展数据',
                            default: '{k1:1, k2:"测试", k3:null}'
                        },
                    ],
                    bodyParams: [],
                    headersParams: [param_header_token],
                    result: {
                        success: '{status:1, msg:"", data:null, code:0}',
                        error: '{status:0, msg:"错误消息", data:null, code:0}',
                    }
                },
                // 支持目录功能
                {
                    title: '日志模块',
                    isFolder: true, // 是否目录
                    api_data: [
                        // 示例接口2
                        {
                            title: '更新日志',
                            intro: '',
                            created_at: new Date().toLocaleDateString(),
                            updated_at: new Date().toLocaleDateString(),
                            method: 'POST',
                            url: '/index.php/api/log/update',
                            querystringParams: [],
                            bodyParams: [
                                param_id, {
                                    name: 'content',
                                    required: true,
                                    dataType: 'string',
                                    inputType: 'textarea',
                                    comment: '日志描述',
                                    default: ''
                                },
                            ],
                            headersParams: [param_header_token],
                            result: {
                                success: '{status:1, msg:"", data:key字符串, code:0}',
                                error: '{status:1, msg:"错误消息", data:null, code:0}',
                            }
                        }, {
                            title: '查询日志详情',
                            intro: '',
                            method: 'POST',
                            url: '/index.php/api/log/read',
                            querystringParams: [],
                            bodyParams: [param_id],
                            headersParams: [param_header_token],
                            result: {
                                success: '{status:1, msg:"", data:key字符串, code:0}',
                                error: '{status:1, msg:"错误消息", data:null, code:0}',
                            }
                        }, {
                            title: '查询日志列表',
                            intro: '',
                            method: 'POST',
                            url: '/index.php/api/log/list',
                            querystringParams: [],
                            bodyParams: [param_page, param_page_size],
                            headersParams: [param_header_token],
                            result: {
                                success: '{status:1, msg:"", data:key字符串, code:0}',
                                error: '{status:1, msg:"错误消息", data:null, code:0}',
                            }
                        }, {
                            title: '删除日志列表',
                            intro: '',
                            method: 'DELETE',
                            url: '/index.php/api/log/delete',
                            querystringParams: [],
                            bodyParams: [param_ids, param_appid, param_appkey, param_timestamp, param_nonce, param_sign],
                            headersParams: [param_header_token],
                            signHandler: function (elTable) {
                                let params = {
                                    id: elTable.querySelector('input[name="id"]').value,
                                    appid: elTable.querySelector('input[name="appid"]').value,
                                    appkey: elTable.querySelector('input[name="appkey"]').value,
                                    timestamp: elTable.querySelector('input[name="timestamp"]').value,
                                    nonce: elTable.querySelector('input[name="nonce"]').value
                                }
                                // 按参数名称从小到大的顺序重新排列
                                let query = `appid=${params.appid}&appkey=${params.appkey}&id=${params.id}&nonce=${params.nonce}&timestamp=${params.timestamp}`;
                                // 用md5加密重新串联成请求字符串，截取中间16位作为签名
                                var sign = md5(query).substr(8, 16);
                                console.log('==签名', query, sign, md5(query));
                                elTable.querySelector('input[name="sign"]').value = sign;
                            },
                            result: {
                                success: '{status:1, msg:"", data:key字符串, code:0}',
                                error: '{status:1, msg:"错误消息", data:null, code:0}',
                            }
                        },
                    ]
                }
            ]
        };
    }

    // 页面初始化
    pageInit();

</script>

</html>